Make examples more regularized and focused, and add contribution guidelines for the examples folder#928
Merged
Conversation
…elines for the examples folder
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #928 +/- ##
=======================================
Coverage 62.41% 62.41%
=======================================
Files 25 25
Lines 4497 4497
=======================================
Hits 2807 2807
Misses 1690 1690
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
jeremiah-k
added a commit
to jeremiah-k/mtjk
that referenced
this pull request
Jun 1, 2026
…htastic#928 Ports the 6 new files from upstream master 81ae8b6 (PR meshtastic#928 by ianmcorvidae): - examples/CONTRIBUTING.md - examples/tcp_connection_info_once.py - examples/tcp_pubsub_send_and_receive.py (replacement for pub_sub_example*.py) - examples/meshtastic_serial_message_reader.py - examples/textchat.py - examples/replymessage.py The rewrites of existing example files in 81ae8b6 (get_hw.py, hello_world_serial.py, info_example.py, scan_for_devices.py, set_owner.py, show_ports.py, tcp_gps_example.py, waypoint.py) are skipped because develop's versions are already more robust (context managers, type hints, stricter error handling) than master's modernization. Upstream commit: 81ae8b6 (Make examples more regularized and focused) Original author: Ian McEwen <ian@ianmcorvidae.net>
jeremiah-k
added a commit
to jeremiah-k/mtjk
that referenced
this pull request
Jun 1, 2026
* Added example script : meshtastic_serial_message_reader.py * Updates and bug fixes meshtastic_serial_message_reader.py * Adjusting old deprecation test and exception for non-abstract use of StreamInterface * Removing superfluous setting of noProto in init() * Updating test for change of deprecation exception * Shifting serial interface connection parts from __init__() into connect() method * Removing unnecessary initialization of self.stream in TCPInterface * Reorganising connect method calls for when connectNow is false * Linting adjustment for change to StreamInterface non-abstract use check * Container: Add initial container for meshtastic-cli Just a quick set of files to enable the build of (tagged) containers. Both alpine and debian containers are available (~200MiB/~1.2GiB) allowing us to use meshtastic cli with a quick docker run, instead of having to build/install stuff locally. Signed-off-by: Olliver Schinagl <oliver@schinagl.nl> * Filter --reply based on specified channel index Ensures that automatic replies are sent back on the same channel index the message was received on. Previously, all replies defaulted to the primary channel (0), even if the incoming message arrived on a secondary channel. Additionally it ensures incoming messages match the specified channel index. E.g: meshtastic --ch-index 1 --reply . Modified the `onReceive` handler to extract the `channel` index from received packets. This ensures `interface.sendText` targets the originating channel rather than always defaulting to the primary channel. Added a filter to ensure that only the specified channel index is being replied to. * fix(ble): handle BLEError with user-friendly messages Replace raw tracebacks with helpful error messages that explain: - What went wrong - Possible causes - How to fix it Covers all BLEError cases: - Device not found (BLE disabled, sleep mode, out of range) - Multiple devices found (need to specify which one) - Write errors (pairing PIN, Linux bluetooth group) - Read errors (device disconnected) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * fix(cli): add timeout error handling for serial connections Handle MeshInterface.MeshInterfaceError when device is rebooting or connection times out, with user-friendly error message. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * Refactor the Meshtastic TCP pub/sub example to ensure proper resource cleanup and clearer exception handling. * Give TCPInterface reconnect logic on write errors * Moving to socket.sendall() is safer, as sendall will send the entire buffer, while send() would return the number of bytes sent and require being called multiple times if the buffer was full. * On exceptions: reconnect to the server. * On reconnection: make sure using a lock that there isn't a race between the readers and the writers triggering a reconnect. * Fix local node position overwrite by low-precision echoes When other nodes relay our position via map reports, they send it at reduced precision (e.g., 13 bits). _onPositionReceive() was blindly overwriting our locally-stored high-precision GPS position (32 bits) with these degraded echoes. The fix only protects the local node's position — since we have the GPS internally, any lower-precision update is always an echo from the mesh, never fresh data. Remote node positions are still updated normally, as any position they broadcast reflects their current state. Fixes meshtastic#910 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Added textchat.py and replymessage.py example scripts from TODO * protobufs: v2.7.21 * loosen packaging version requirement * poetry lock * Update factory reset to use integer for config reset * StreamInterface: prevent socket/reader-thread leak on handshake failure in __init__ If connect() or waitForConfig() raises during __init__ (handshake timeout, bad stream, config error), the reader thread started by connect() keeps running and the underlying stream/socket stays open — but the caller never receives a reference to the half-initialized instance, so they cannot call close() themselves. The leak compounds on every retry from a caller's reconnect loop. Fix: wrap connect() + waitForConfig() in try/except; call self.close() on any exception before re-raising. Also guard close() against RuntimeError from joining an unstarted reader thread (happens when close() runs from a failed __init__ before connect() could spawn it). Discovered while debugging a real-world Meshtastic firmware crash where a passive logger's retrying TCPInterface() calls against a node with 250-entry NodeDB produced a reconnect storm — every retry triggered a full config+NodeDB dump on the node, compounding heap pressure, which then exposed null-deref bugs in Router::perhapsDecode / MeshService (firmware side fixed in meshtastic/firmware#10226 and #10229). The client-side leak is independent of those firmware bugs and worth fixing on its own. * protobufs: v2.7.24 * Inject options in nanopb .options files into the protobuf files used for code generation * Add tests of nanopb options injection * pylint cleanups * the fuzz * fix import order * Update the other factory reset to use integer too * Document the use of --ch-index along with --reply a little * pylint fix * fix a missing paren in a comment * Fix some leaks/hangs on close: unstarted StreamInterface streams & TCP reader unblock * pylint strikes again * pylint/test fixes * some pre-merge cleanup * Re-establish the OSError being raised to match former behavior, but still reconnect. TBD if this is quite the right approach. * Avoid deadlocking on potentially re-entrant _startConfig call, and don't reconnect when _wantExit * make test more deterministic for reconnect count testing * Harden a bit, update some sections, add a README section * A bunch of dependency updates in poetry.lock (to shut up Dependabot) * tryfix container build issue * Remove arm/v7 from the container build right now, can't be bothered * Make examples more regularized and focused, and add contribution guidelines for the examples folder * Add a BLEError 'kind' field and branch on it instead of string matching * tests of new BLEError functionality * More lockfile updates now that dependabot woke up * One more dependency update * Filter --reply based on specified channel index Ensures that automatic replies are sent back on the same channel index the message was received on. Previously, all replies defaulted to the primary channel (0), even if the incoming message arrived on a secondary channel. Additionally it ensures incoming messages match the specified channel index. E.g: meshtastic --ch-index 1 --reply . Modified the `onReceive` handler to extract the `channel` index from received packets. This ensures `interface.sendText` targets the originating channel rather than always defaulting to the primary channel. Added a filter to ensure that only the specified channel index is being replied to. * Container: Add initial container for meshtastic-cli Just a quick set of files to enable the build of (tagged) containers. Both alpine and debian containers are available (~200MiB/~1.2GiB) allowing us to use meshtastic cli with a quick docker run, instead of having to build/install stuff locally. Signed-off-by: Olliver Schinagl <oliver@schinagl.nl> * Document the use of --ch-index along with --reply a little * Add new example scripts and contribution guidelines from upstream meshtastic#928 Ports the 6 new files from upstream master 81ae8b6 (PR meshtastic#928 by ianmcorvidae): - examples/CONTRIBUTING.md - examples/tcp_connection_info_once.py - examples/tcp_pubsub_send_and_receive.py (replacement for pub_sub_example*.py) - examples/meshtastic_serial_message_reader.py - examples/textchat.py - examples/replymessage.py The rewrites of existing example files in 81ae8b6 (get_hw.py, hello_world_serial.py, info_example.py, scan_for_devices.py, set_owner.py, show_ports.py, tcp_gps_example.py, waypoint.py) are skipped because develop's versions are already more robust (context managers, type hints, stricter error handling) than master's modernization. Upstream commit: 81ae8b6 (Make examples more regularized and focused) Original author: Ian McEwen <ian@ianmcorvidae.net> * Add bin/inject_nanopb_options.py for nanopb options injection Ports the inject_nanopb_options.py script from upstream master 89d81c9. The script reads .options files from the protobufs submodule and injects the nanopb constraints (max_size, max_length, etc.) as inline proto field options so protoc --python_out embeds them in the generated descriptors. Python code can then read them via: field.GetOptions().Extensions[nanopb_pb2.nanopb].max_size Includes a7d13eb's parse_value bug fix: replaced s.lstrip("-").isdigit() with re.fullmatch(r"-?[0-9]+", s) to correctly reject strings like "---5" that lstrip would mangle. The actual _pb2.py regeneration is intentionally NOT performed here — the protobufs submodule is regenerated by CI workflows (see .github/workflows/update_protobufs.yml), and the script is ready for the next regen. Upstream commits: 89d81c9 (script) + a7d13eb (parse_value fix) Original author: Ian McEwen <ian@ianmcorvidae.net> * Integrate nanopb options injection into bin/regen-protobufs.sh Adds the upstream 89d81c9 injection step to develop's regen script: 1. Copies protobufs/meshtastic/*.options into the temp build dir 2. After the existing sed pipeline, iterates over .options files and calls bin/inject_nanopb_options.py on the matching .proto file 3. Then runs protoc as before, so the generated _pb2.py descriptors embed the nanopb constraints inline The script change is intentionally additive: if no .options files are present, the for loop is a no-op, so existing regeneration behavior is preserved. The actual _pb2.py files are NOT regenerated in this commit — the protobufs submodule is auto-regenerated by CI workflows (per copilot instructions: 'Never edit _pb2.py or _pb2.pyi files directly. Regenerate with: make protobufs or ./bin/regen-protobufs.sh'). The next CI run will pick up the new injection step. Adapted to develop's variable style (${SEDCMD[@]} array, ${PROTOC} variable) rather than master's hardcoded paths. Upstream commit: 89d81c9 Original author: Ian McEwen <ian@ianmcorvidae.net> * Add tests for bin/inject_nanopb_options.py (nanopb options injection) Ports meshtastic/tests/test_inject_nanopb_options.py from upstream master 280323d. The test file has two parts: Part 1 (test_parse_*, test_inject_*): unit-tests the script's logic directly using small synthetic proto snippets and a tmp_path-based test harness. Covers parse_value, parse_options_file, apply_options, and inject. Part 2 (test_descriptor_*): smoke-tests the already-generated _pb2.py files to confirm the regen pipeline embedded the expected nanopb options. These will pass once the next CI protobuf regeneration runs (the protobufs submodule is auto-regenerated by .github/workflows/update_protobufs.yml, which now invokes the updated bin/regen-protobufs.sh). Includes a7d13eb's three hypothesis property-based tests for parse_value: - test_parse_value_any_integer_returns_int: any int string round-trips - test_parse_value_never_crashes: no input crashes the parser - test_parse_value_non_numeric_non_bool_returns_str: non-numeric non-bool strings pass through unchanged The hypothesis dependency is already in pyproject.toml per the project's copilot-instructions.md tech stack. The test file loads bin/inject_nanopb_options.py at test time via importlib.util.spec_from_file_location, so it does not require the script to be on PYTHONPATH. Upstream commits: 280323d (test file) + a7d13eb (hypothesis tests) Original author: Ian McEwen <ian@ianmcorvidae.net> * Fix CI: regenerate protobufs with nanopb injection, fix reply filter test, fix lint - Regenerated all _pb2.py files via bin/regen-protobufs.sh to embed nanopb options (max_size, max_count, int_size) in protobuf descriptors. The inject script was already integrated in commit 1e65a62 but _pb2.py files hadn't been regenerated since then. Fixes 10 test_descriptor_* test failures. - test_main_onReceive_with_text: set args.reply=True and args.ch_index=None explicitly. MagicMock's default __int__ returns 1, making targetChannel=1 != rxChannel=0, so the reply was being silently filtered by the --ch-index logic (cherry-picked from upstream c8b1b8e). Fixes the 'Ignored message on channel 0' → 'Sending reply' assertion. - test_inject_nanopb_options.py: fix ruff lint (unused imports, docstring caps, ambiguous 'l' vars, unused 'lines'/'user_line' variables). - __main__.py: fix trailing whitespace (pylint C0303). Each update should complete the CI checks. * Apply code review fixes (2 critical, 4 medium) Critical: - examples/replymessage.py: prevent infinite auto-reply loop by filtering self-sent messages (from == my_node_num) and auto-reply echoes (text starts with "got msg '") - __main__.py --reply: same loop prevention — without this, a node running --reply would reply to its own replies, spamming the mesh Medium: - inject_nanopb_options.py: import detection now matches lines with trailing comments (uses ";" in line instead of endswith(";")) - inject_nanopb_options.py: explicit encoding='utf-8' on file open for Windows compatibility - examples/textchat.py: filter local echo messages so user doesn't see their own messages duplicated - Containerfile.alpine: add --no-directory flag to match Debian variant and avoid unnecessary local copy into system site-packages Also fixes ruff lint (D400/D401/D403 docstring style) in touched files. * Apply 13 review findings: security, correctness, style container-build.yaml: - Remove continue-on-error: true (was masking build failures) - Add persist-credentials: false to checkout step - Pin all 6 action refs to immutable commit SHAs Containerfile.alpine + Containerfile.debian: - Add non-root user (meshtastic) before ENTRYPOINT bin/inject_nanopb_options.py: - Replace typing.Dict/List/Tuple with PEP 585 built-in generics - Fix scope merge bug: remove break so all matching specific keys get merged (shortest path first, more-specific overrides) meshtastic/tests/test_inject_nanopb_options.py: - Add type annotations to _load_inject_module, _inject, _field_opts examples/replymessage.py: - Send reply on received channel (channelIndex=) not default - Wrap executable code in main() with __name__ guard - Use PEP 604 unions (X | None) instead of Optional/Union examples/textchat.py: - Wrap executable code in main() with __name__ guard - Use PEP 604 unions instead of Optional/Union examples/CONTRIBUTING.md: - Add checklist item 8: type hints + PEP 604/built-in generics meshtastic/__main__.py: - Fix --reply channel filter: unset --ch-index now means any channel (was incorrectly defaulting to channel 0) - Use .get() with fallback for rxSnr/hopLimit (crash on missing fields) * Fix mypy + pylint failures on review changes - test_inject_nanopb_options.py: add assert guards for None checks on spec_from_file_location return (mypy arg-type/union-attr) - inject_nanopb_options.py main(): add docstring, encoding='utf-8' on read_text/write_text (pylint missing-docstring, unspecified-encoding) - examples/replymessage.py main(): add docstring (pylint C0116) - examples/textchat.py main(): add docstring (pylint C0116) * style: reformat code and update CI workflow Apply consistent code formatting across examples, main entry point, and tests to improve readability and adhere to style guidelines. Update GitHub Actions workflow to use double quotes for tag patterns. * Address review feedback: container CI, examples polish, tests, determinism Container workflow (.github/workflows/container-build.yaml): - Add 'develop' to push and pull_request branch triggers - Remove linux/arm/v7 and linux/arm/v6 platforms (upstream build issues) - Update autotag logic to use 'auto' for any branch push Examples (replymessage.py, textchat.py): - Type packet parameter as dict[str, Any] instead of bare dict - Add Interface type alias to replace verbose multi-line union types - Use 'exc' naming for caught exceptions (pylint W0707) - Move pylint disable comment to correct line for onConnection inject_nanopb_options.py: - Sort option keys in format_nanopb_opts() for deterministic output across Python versions and option file ordering New tests — onReceive reply behavior (test_main.py, 4 tests): - test_main_onReceive_reply_uses_rx_channel: verifies channelIndex=packet channel - test_main_onReceive_ch_index_filter_mismatch: verifies --ch-index mismatch ignored - test_main_onReceive_own_packet_no_reply: verifies own-node packets skipped - test_main_onReceive_auto_reply_echo_no_reply: verifies 'got msg' prefix skipped New tests — inject_nanopb_options edge cases (7 tests): - Comments with braces don't corrupt nesting - Map<> fields are skipped (not injected) - Oneof fields receive options correctly - Enum value lines are not modified - format_nanopb_opts output is sorted/deterministic - Duplicate specific/wildcard options merge deterministically - Multiple close braces on one line handled correctly Protobuf reproducibility verified: regen-protobufs.sh produces clean diff. Pre-existing failures: test_main_init_parser_version and test_main_main_version fail due to version detection in dev environment (not related to this change). * Fix duplicate pull_request key, centralize project name constants container-build.yaml: - Fix critical YAML duplicate key bug: merge two pull_request blocks into one with both master and develop branches - Remove invalid 'tags' subkey from pull_request event version.py: - Add PACKAGE_NAME, PROJECT_DISPLAY_NAME, INSTALL_UPGRADE_HINT constants - Single source of truth for project identity; swap PACKAGE_NAME to 'meshtastic' when upstreaming util.py: - Import DISTRIBUTION_NAME_CANDIDATES from version.py instead of duplicating the definition __main__.py: - Use PROJECT_DISPLAY_NAME and INSTALL_UPGRADE_HINT from version.py - Upgrade hint now recommends 'pipx upgrade mtjk' instead of 'pip install --upgrade meshtastic' ble_interface.py, interfaces/ble/__init__.py: - Update BLE bleak install error to recommend 'pipx install mtjk' * Remove Containerfile.alpine from container build matrix * Restore alpine matrix entry, skip build outside upstream repo Only builds Containerfile.alpine when running in meshtastic/python. Any fork (including mtjk) automatically skips it. One line to remove when upstreaming. * ci(container): update alpine build conditional comment Update the comment in the container build workflow to clarify that the Alpine build is restricted to the upstream repository. * refactor(ci): restrict all container builds to upstream repository Update the job conditional to ensure that container builds are only executed within the meshtastic/python repository. This simplifies the logic previously used to selectively skip the Alpine build by applying the restriction to the entire job. --------- Signed-off-by: Olliver Schinagl <oliver@schinagl.nl> Signed-off-by: Aleksei Sviridkin <f@lex.la> Co-authored-by: henri <github.shustak@neverbox.com> Co-authored-by: Travis-L-R <> Co-authored-by: Olliver Schinagl <oliver@schinagl.nl> Co-authored-by: Rob <95710162+thatSFguy@users.noreply.github.com> Co-authored-by: Aleksei Sviridkin <f@lex.la> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: dim5x <dim5x@yahoo.com> Co-authored-by: Stephen Thorne <stephen@thorne.id.au> Co-authored-by: Benjamin Babeshkin <skypanther@gmail.com> Co-authored-by: Aron Tkachuk <arotkac@icloud.com> Co-authored-by: Ian McEwen <ian@ianmcorvidae.net> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: nightjoker7 <mattdeering7@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Includes the example from #739 as part of this.
This should mean a little less anything-goes in the examples folder, hopefully. Probably still more we could do to make that nice, but this is an okay start.